package com.miracle.shiro.config;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;

import java.util.HashSet;
import java.util.Set;

/**
 * @Author: rupert-tears
 * @Date: Created in 11:47 2021/7/16
 * @Description: 优秀的人都在孤独的翻山越岭！
 * 自定义Realm类，利用这个类来完成用户的身份验证和授权
 * 实现一个Realm的父接口，但是这个接口没有什么具体的功能
 * 如果需要完成对用户的身份验证和授权 需要继承Realm的其他抽象子类
 * Realm的子类
 * AuthenticatingRealm  专门用于完成用户身份验证的抽象父类
 * AuthorizingRealm 可实现用户的身份认证以及权限分配
 */
public class MyRealm extends AuthorizingRealm {
    /**
     * 用户身份验证的具体方法
     * doGetAuthenticationInfo 方法 (不能手动调用)
     * @param authenticationToken 用户身份令牌 这个对象中存放着用户在浏览器中输入的账号和密码
     * @return 返回验证密码的对象 Shiro就会自动根据对象中的数据来验证密码是否正确
     *         验证失败会自动抛出  IncorrectCredentialsException：密码错误异常 （Shiro自动抛出）
     * @throws AuthenticationException 如果验证失败，需要手动抛出异常
     *         AccountException：用户账号异常 手动抛出
     *         UnknownAccountException：账号错误异常 手动抛出
     *         LockedAccountException：账号被冻结异常 手动抛出
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 获取用户账号对象 (强制类型转换)
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        // 获取用户在页面中输入的用户账号
        String username = token.getUsername();
        /*
         * 验证账号是否正确
         * 实际工作中，应该从数据库中获取数据
         * 魔法值
         * 实例账号 admin（正常账号） zhangsan（被冻结账号）
         */
        if (!"admin".equals(username)&&!"zhangsan".equals(username)&&!"lisi".equals(username)
        &&!"admin02".equals(username)){
            throw new UnknownAccountException("账号输入错误");
        }else if ("zhangsan".equals(username)){
            throw new LockedAccountException("账号被冻结");
        }
        // 获取当前操作对象
        Subject subject = SecurityUtils.getSubject();
        // 通过Subject中的Session会话 来获取盐（前端请求后端产生的加密过的盐）
        // Subject 中的 Session 会话，与应用容器中（Tomcat）中的HttpSession是通用的  取出盐
        String randomSalt = (String) subject.getSession().getAttribute("randomSalt");
        // 数据库中的密码 需要通过账号进行获取
        String dbPassword = "990728";
        // 使用随机盐 对数据库中的密码进行加密   参数：加密方式 数据库密码 随机盐
        String password = new SimpleHash("MD5",dbPassword,randomSalt).toString();
        // 返回验证密码的对象
        return new SimpleAuthenticationInfo(username,password,this.getName());
    }

    /**
     * 完成用户授权的方法
     * @param principalCollection 触发权限验证当前用户的账户
     * @return 返回验证对象
     * Shiro每次会判断当前请求是否有权限时，都会调用这个方法来获取用户的具体权限信息
     * 因此可能造成重复访问数据库获取权限数据，所有在这里要添加一些逻辑控制
     * 例如 获取玩权限后存入缓存或者Session会话中，这样下一次进入时，就可以就可以不用访问数据库了
     * 但是这样的话，带来了一个弊端，不和数据库进行交互 就不能获得实时的权限信息
     * 如果用户的权限被更新后 必须要重新登录才能刷新权限信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 获取触发权限验证当前用户的账户
        String username = (String) principalCollection.getPrimaryPrincipal();
        // 定义一个用于存放用户角色的Set集合
        Set<String> roles = new HashSet<>();
        // 定义一个用于存放用户权限的Set集合
        Set<String> perms = new HashSet<>();
        // 根据账号设置权限，实际工作时应该是从数据库中获取权限
        if ("admin".equals(username)){
            roles.add("admin");
            roles.add("user");
        }else if ("lisi".equals(username)){
            roles.add("user");
        }else if ("admin02".equals(username)){
            roles.add("admin");
            perms.add("admin:add");
        }
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        // 设置角色权限验证的对象中
        simpleAuthorizationInfo.setRoles(roles);
        // 设置权限
        simpleAuthorizationInfo.setStringPermissions(perms);
        // 返回验证对象，Shiro就会获取这个对象中的角色以及权限集合来判断当前用户是否有权限执行这个操作
        return simpleAuthorizationInfo;
    }
}
